using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using TelerikMvcGridCustomBindingHelper.Exceptions;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.Mapper
{
    static class GridModelMapperBase<TEntity, TViewModel>
    {
        private static ConcurrentDictionary<string, PropertyMap> PropertyMapCache
        {
            get
            {
                return GridModelMapper.ConfigurationObject.TinyCache.GetOrSetItem("_propertyMapCacheKey-{e36b1a92-d6fa-4268-966f-15488b161bf2}",
                    () => new ConcurrentDictionary<string, PropertyMap>());
            }
        }

        public static IMappingExpression<TEntity, TViewModel> CreateMap()
        {
            if (AutoMapper.Mapper.FindTypeMapFor<TEntity, TViewModel>() == null)
            {
                AutoMapper.Mapper.CreateMap<TEntity, TViewModel>();
            }

            return new MappingExpression<TEntity, TViewModel>(null);
        }

        public static IMappingExpressionWithOptions<TEntity, TViewModel> MapProperty<TEntityProperty, TViewModelProperty>(Expression<Func<TEntity, TEntityProperty>> entityPropertySelector, Expression<Func<TViewModel, TViewModelProperty>> viewModelPropertySelector)
        {
            var gridPropertyInfo = new GridPropertyInfo(entityPropertySelector, viewModelPropertySelector, typeof(TEntityProperty), typeof(TViewModelProperty));

            AddPropertyMap(gridPropertyInfo, viewModelPropertySelector.GetPropertyPath());

            if (CanPassToAutoMapper(entityPropertySelector, viewModelPropertySelector))
            {
                var convert = Expression.Convert(viewModelPropertySelector.Body, typeof(object));
                var lambdaExpression = Expression.Lambda<Func<TViewModel, object>>(convert, viewModelPropertySelector.Parameters);

                AutoMapper.Mapper.CreateMap<TEntity, TViewModel>()
                    .ForMember(lambdaExpression, opt => opt.MapFrom(entityPropertySelector));
            }

            return new MappingExpression<TEntity, TViewModel>(gridPropertyInfo);
        }

        private static bool CanPassToAutoMapper<TEntityProperty, TViewModelProperty>(Expression<Func<TEntity, TEntityProperty>> entityPropertySelector, Expression<Func<TViewModel, TViewModelProperty>> viewModelPropertySelector)
        {
            var viewModelPropertyPath = viewModelPropertySelector.GetPropertyPath();

            // AutoMapper can't handle complex properties
            if (viewModelPropertyPath.Contains('.'))
                return false;

            var propertyMaps = AutoMapper.Mapper.FindTypeMapFor<TEntity, TViewModel>().GetPropertyMaps();

            try
            {
                foreach (var propertyMap in propertyMaps)
                {
                    var sourceMemberName = string.Empty;

                    if (propertyMap.SourceMember != null)
                        sourceMemberName = propertyMap.SourceMember.Name;

                    if (string.IsNullOrWhiteSpace(sourceMemberName) && propertyMap.CustomExpression != null)
                        sourceMemberName = propertyMap.CustomExpression.GetPropertyPath();

                    if (string.IsNullOrWhiteSpace(sourceMemberName))
                        continue;

                    var entityPropertyName = Reflector.GetMemberName(entityPropertySelector);
                    if (sourceMemberName.Equals(entityPropertyName) ||
                        sourceMemberName.Equals(viewModelPropertyPath))
                        return false;

                    if (propertyMap.DestinationProperty.Name.Equals(viewModelPropertyPath))
                        return false;
                }
            }
            catch
            {
                return false;
            }

            return true;
        }

        public static GridPropertyInfo GetPropertyMap(string viewModelPropertyName, Type entity = null, Type viewmodel = null)
        {
            var typeofEntity = typeof(TEntity);
            var typeofViewModel = typeof(TViewModel);

            if (entity != null && viewmodel != null)
            {
                typeofEntity = entity;
                typeofViewModel = viewmodel;
            }

            // Trying get the entity property in our custom maps
            if (ExistMapFor(viewModelPropertyName, typeofEntity, typeofViewModel))
            {
                var propertyMap = PropertyMap.Stub(typeofEntity, typeofViewModel);

                if (PropertyMapCache.TryGetValue(propertyMap.Key, out propertyMap))
                {
                    GridPropertyInfo entityMemberInfo;
                    propertyMap.Properties.TryGetValue(viewModelPropertyName, out entityMemberInfo);
                    return entityMemberInfo;
                }
            }
            else
            {
                if (viewModelPropertyName.IsNotNullOrWhiteSpace() && viewModelPropertyName.Contains("."))
                {
                    var parts = viewModelPropertyName.Split('.');

                    var gridPropertyInfo = GetPropertyMap(parts[0]);

                    if (!gridPropertyInfo.IsArtificial())
                    {
                        var currentPath = parts.Skip(1).Aggregate(string.Empty, (current, part) => current + part);

                        var parent = GetPropertyMap(currentPath, gridPropertyInfo.PropertyType, gridPropertyInfo.ViewModelPropertyType);

                        var leftExpr = gridPropertyInfo.EntityProperty;
                        var rightExpr = parent.EntityProperty ?? parent.CustomExpression;
                        
                        var entityProperty = ParameterToMemberExpressionRebinder.CombinePropertySelectorWithPredicate<TEntity, object>(leftExpr, rightExpr);
                        var viewModelProperty = ParameterToMemberExpressionRebinder.CombinePropertySelectorWithPredicate<TViewModel, object>(gridPropertyInfo.ViewModelProperty, parent.ViewModelProperty);

                        gridPropertyInfo = new GridPropertyInfo(entityProperty, viewModelProperty, Reflector.GetMemberType(entityProperty), Reflector.GetMemberType(viewModelProperty));

                        AddPropertyMap(gridPropertyInfo, viewModelPropertyName);

                        return gridPropertyInfo;
                    }
                }
            }

            // Property not found, try get from AutoMapper
            var propertyMapFromAutoMapper = GetPropertyMapFromAutoMapper(typeofEntity, typeofViewModel, viewModelPropertyName);
            if (propertyMapFromAutoMapper != null)
                return propertyMapFromAutoMapper;

            // Property was not mapped, trying extract direct from the entity type
            //var propertyInfo = Reflector.GetMemberInfo<TEntity>(viewModelPropertyName).As<PropertyInfo>();
            var propertyInfo = Reflector.GetMemberInfo(typeofEntity, viewModelPropertyName).As<PropertyInfo>();
            if (propertyInfo != null)
                return new GridPropertyInfo(propertyInfo.PropertyType, viewModelPropertyName);

            throw new PropertyNotFoundException(typeofEntity, viewModelPropertyName);
        }

        public static List<GridPropertyInfo> GetPropertyMaps(Type sourceType, Type targetType)
        {
            var propertyMap = PropertyMap.Stub(sourceType, targetType);

            var existMapForTypes = PropertyMapCache.TryGetValue(propertyMap.Key, out propertyMap);

            return existMapForTypes ? propertyMap.Properties.Select(pair => pair.Value).ToList() : null;
        }

        public static List<GridPropertyInfo> GetPropertyMaps()
        {
            return PropertyMapCache.Select(pair => pair.Value.Properties).SelectMany(pair => pair.Values).ToList();
        }

        private static GridPropertyInfo GetPropertyMapFromAutoMapper(Type sourceType, Type targetType, string viewModelPropertyName, string rootPath = "")
        {
            //todo: update with AutoMapper codebase
            var propertyPath = string.Empty;
            Type propertyType = null;

            // Get the AutoMapper maps for the given source and target types
            var map = AutoMapper.Mapper.FindTypeMapFor(sourceType, targetType);

            if (map == null) return null;

            var propertyMaps = map.GetPropertyMaps();

            // Loop through the mapped properties
            foreach (var propertyMap in propertyMaps)
            {
                // Find the mapped property for the given TViewModel (ComplexTypes)
                if (viewModelPropertyName.Contains("."))
                {
                    if (!propertyMap.DestinationProperty.Name.Equals(viewModelPropertyName.Split('.')[0]))
                        continue;

                    // Check if its comming from a custom expression (in this case the mapping for
                    // source property was given by the developer with the help of the method ForMember)
                    if (propertyMap.CustomExpression != null)
                    {
                        var sourcetype = Reflector.GetMemberInfo(propertyMap.CustomExpression).As<PropertyInfo>().PropertyType;
                        var targettype = propertyMap.DestinationProperty.MemberType;
                        var pathSkipedFirstPart = viewModelPropertyName.Split('.').Skip(1).Aggregate(string.Empty, (current, tmp) => (string.Concat(current, ".", tmp))).Remove(0, 1);
                        var rootpath = string.Concat(rootPath, propertyMap.CustomExpression.GetPropertyPath());

                        // For a complex type we have to call the same method again
                        return GetPropertyMapFromAutoMapper(sourcetype, targettype, pathSkipedFirstPart, rootpath);
                    }
                }

                // Find the mapped property for the given TViewModel (PrimitiveTypes)
                if (!propertyMap.DestinationProperty.Name.Equals(viewModelPropertyName))
                    continue;

                // Gets the type of the source property
                var sourceMember = propertyMap.SourceMember as PropertyInfo;
                if (sourceMember != null)
                    propertyType = sourceMember.PropertyType;

                // Check if its comming from a custom expression
                if (propertyMap.CustomExpression != null)
                {
                    propertyPath = propertyMap.CustomExpression.GetPropertyPath();
                    //propertyType = propertyType ?? Reflector.GetMemberInfo<TEntity>(propertyPath).As<PropertyInfo>().PropertyType;
                    propertyType = propertyType ?? Reflector.GetMemberInfo(sourceType, propertyPath).As<PropertyInfo>().PropertyType;
                }
                else
                {
                    foreach (var sourceValueResolver in propertyMap.GetSourceValueResolvers())
                    {
                        // Target = AutoMapper.Internal.PropertyGetter.Name
                        // It took a bit of Reflection here since it is a private property
                        var propertyInfo = sourceValueResolver.GetType().GetProperty("Name");
                        if (propertyInfo == null) continue;
                        var propValue = propertyInfo.GetValue(sourceValueResolver, null) as string;
                        propertyPath = string.Concat(propertyPath, ".", propValue);
                    }
                }

                // Removes the "." from the front of the string
                if (propertyPath.StartsWith("."))
                    propertyPath = propertyPath.Remove(0, 1);

                // It cant be found in until here, lets try return the SourceMember.Name
                if (propertyPath.Length == 0 && sourceMember != null)
                    propertyPath = sourceMember.Name;

                // Add the rootPath If we have one
                if (!string.IsNullOrWhiteSpace(rootPath))
                    propertyPath = string.Concat(rootPath, ".", propertyPath);

                // Provide some preliminar validation
                if (propertyType == null || string.IsNullOrWhiteSpace(propertyPath) || propertyPath.StartsWith(".") || propertyPath.EndsWith("."))
                    return null;

                // Finally we have our so desired GridPropertyInfo
                return new GridPropertyInfo(propertyType, propertyPath);
            }

            // Ahrg... Nothing found on AutoMapper too
            return null;
        }

        private static void AddPropertyMap(GridPropertyInfo gridPropertyInfo, string viewModelPropertyName)
        {
            if (ExistMapFor(viewModelPropertyName))
                return;

            var newMap = PropertyMap.Stub<TEntity, TViewModel>();

            if (PropertyMapCache.TryGetValue(newMap.Key, out newMap))
            {
                newMap.AddProperty(gridPropertyInfo, viewModelPropertyName);
                PropertyMapCache.TryUpdate(newMap.Key, newMap, newMap);
            }
            else
            {
                newMap = new PropertyMap(typeof(TEntity), typeof(TViewModel), gridPropertyInfo, viewModelPropertyName);
                PropertyMapCache.TryAdd(newMap.Key, newMap);
            }
        }

        private static bool ExistMapFor(string viewModelPropertyName, Type entity = null, Type viewmodel = null)
        {
            var typeofEntity = entity ?? typeof(TEntity);
            var typeofViewModel = viewmodel ?? typeof(TViewModel);

            if (viewModelPropertyName.IsNullOrWhiteSpace())
                return false;

            var propertyMap = PropertyMap.Stub(typeofEntity, typeofViewModel);

            return PropertyMapCache.TryGetValue(propertyMap.Key, out propertyMap)
                && propertyMap.Properties.ContainsKey(viewModelPropertyName);
        }
    }
}